/*
 *  Copyright (c) 2013, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

package com.youximao.sdk.lib.common.common.widget.togglebutton.rebound;

import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Classical spring implementing Hooke's law with configurable friction and tension.
 */
public class Spring {

    // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS)
    private static final double MAX_DELTA_TIME_SEC = 0.064;
    // fixed timestep to use in the physics solver in seconds
    private static final double SOLVER_TIMESTEP_SEC = 0.001;
    // unique incrementer id for springs
    private static int ID = 0;
    // unique id for the spring in the system
    private final String mId;
    // all physics simulation objects are final and reused in each processing pass
    private final PhysicsState mCurrentState = new PhysicsState();
    private final PhysicsState mPreviousState = new PhysicsState();
    private final PhysicsState mTempState = new PhysicsState();
    private final BaseSpringSystem mSpringSystem;
    private SpringConfig mSpringConfig;
    private boolean mOvershootClampingEnabled;
    private double mStartValue;
    private double mEndValue;
    private boolean mWasAtRest = true;
    // thresholds for determining when the spring is at rest
    private double mRestSpeedThreshold = 0.005;
    private double mDisplacementFromRestThreshold = 0.005;
    private CopyOnWriteArraySet<SpringListener> mListeners = new CopyOnWriteArraySet<SpringListener>();
    private double mTimeAccumulator = 0;

    /**
     * create a new spring
     */
    Spring(BaseSpringSystem springSystem) {
        if (springSystem == null) {
            throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem");
        }
        mSpringSystem = springSystem;
        mId = "spring:" + ID++;
        setSpringConfig(SpringConfig.defaultConfig);
    }

    /**
     * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be
     * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this,
     * doing so may just cause an exception to be thrown.
     */
    public void destroy() {
        mListeners.clear();
        mSpringSystem.deregisterSpring(this);
    }

    /**
     * get the unique id for this spring
     *
     * @return the unique id
     */
    public String getId() {
        return mId;
    }

    /**
     * retrieve the spring config for this spring
     *
     * @return the SpringConfig applied to this spring
     */
    public SpringConfig getSpringConfig() {
        return mSpringConfig;
    }

    /**
     * set the config class
     *
     * @param springConfig config class for the spring
     * @return this Spring instance for chaining
     */
    public Spring setSpringConfig(SpringConfig springConfig) {
        if (springConfig == null) {
            throw new IllegalArgumentException("springConfig is required");
        }
        mSpringConfig = springConfig;
        return this;
    }

    /**
     * Get the displacement value from the last time setCurrentValue was called.
     *
     * @return displacement value
     */
    public double getStartValue() {
        return mStartValue;
    }

    /**
     * Get the current
     *
     * @return current value
     */
    public double getCurrentValue() {
        return mCurrentState.position;
    }

    /**
     * Set the displaced value to determine the displacement for the spring from the rest value.
     * This value is retained and used to calculate the displacement ratio.
     * This also updates the start value of the Spring.
     *
     * @param currentValue the new start and current value for the spring
     * @return the spring for chaining
     */
    public Spring setCurrentValue(double currentValue) {
        mStartValue = currentValue;
        mCurrentState.position = currentValue;
        mSpringSystem.activateSpring(this.getId());
        for (SpringListener listener : mListeners) {
            listener.onSpringUpdate(this);
        }
        return this;
    }

    /**
     * get the displacement of the springs current value from its rest value.
     *
     * @return the distance displaced by
     */
    public double getCurrentDisplacementDistance() {
        return getDisplacementDistanceForState(mCurrentState);
    }

    /**
     * get the displacement from rest for a given physics state
     *
     * @param state the state to measure from
     * @return the distance displaced by
     */
    private double getDisplacementDistanceForState(PhysicsState state) {
        return Math.abs(mEndValue - state.position);
    }

    /**
     * get the rest value used for determining the displacement of the spring
     *
     * @return the rest value for the spring
     */
    public double getEndValue() {
        return mEndValue;
    }

    /**
     * set the rest value to determine the displacement for the spring
     *
     * @param endValue the endValue for the spring
     * @return the spring for chaining
     */
    public Spring setEndValue(double endValue) {
        if (mEndValue == endValue && isAtRest()) {
            return this;
        }
        mStartValue = getCurrentValue();
        mEndValue = endValue;
        mSpringSystem.activateSpring(this.getId());
        for (SpringListener listener : mListeners) {
            listener.onSpringEndStateChange(this);
        }
        return this;
    }

    /**
     * get the velocity of the spring
     *
     * @return the current velocity
     */
    public double getVelocity() {
        return mCurrentState.velocity;
    }

    /**
     * set the velocity on the spring in pixels per second
     *
     * @return the spring for chaining
     */
    public Spring setVelocity(double velocity) {
        mCurrentState.velocity = velocity;
        mSpringSystem.activateSpring(this.getId());
        return this;
    }

    /**
     * Returns the speed at which the spring should be considered at rest in pixels per second
     *
     * @return speed in pixels per second
     */
    public double getRestSpeedThreshold() {
        return mRestSpeedThreshold;
    }

    /**
     * Sets the speed at which the spring should be considered at rest.
     *
     * @param restSpeedThreshold speed pixels per second
     * @return the spring for chaining
     */
    public Spring setRestSpeedThreshold(double restSpeedThreshold) {
        mRestSpeedThreshold = restSpeedThreshold;
        return this;
    }

    /**
     * get the threshold of displacement from rest below which the spring should be considered at rest
     *
     * @return displacement to consider resting below
     */
    public double getRestDisplacementThreshold() {
        return mDisplacementFromRestThreshold;
    }

    /**
     * set the threshold of displacement from rest below which the spring should be considered at rest
     *
     * @param displacementFromRestThreshold displacement to consider resting below
     * @return the spring for chaining
     */
    public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) {
        mDisplacementFromRestThreshold = displacementFromRestThreshold;
        return this;
    }

    /**
     * Check if overshoot clamping is enabled.
     *
     * @return is overshoot clamping enabled
     */
    public boolean isOvershootClampingEnabled() {
        return mOvershootClampingEnabled;
    }

    /**
     * Force the spring to clamp at its end value to avoid overshooting the target value.
     *
     * @param overshootClampingEnabled whether or not to enable overshoot clamping
     * @return the spring for chaining
     */
    public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) {
        mOvershootClampingEnabled = overshootClampingEnabled;
        return this;
    }

    /**
     * Check if the spring is overshooting beyond its target.
     *
     * @return true if the spring is overshooting its target
     */
    public boolean isOvershooting() {
        return (mStartValue < mEndValue && getCurrentValue() > mEndValue) ||
                (mStartValue > mEndValue && getCurrentValue() < mEndValue);
    }

    /**
     * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required
     * realTimeDelta.
     * The math is inlined inside the loop since it made a huge performance impact when there are
     * several springs being advanced.
     *
     * @param time          clock time
     * @param realDeltaTime clock drift
     */
    void advance(double realDeltaTime) {

        boolean isAtRest = isAtRest();

        if (isAtRest && mWasAtRest) {
      /* begin debug
      Log.d(TAG, "bailing out because we are at rest:" + getName());
      end debug */
            return;
        }

        // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able
        // to catch up in a subsequent advance if necessary.
        double adjustedDeltaTime = realDeltaTime;
        if (realDeltaTime > MAX_DELTA_TIME_SEC) {
            adjustedDeltaTime = MAX_DELTA_TIME_SEC;
        }

    /* begin debug
    long startTime = System.currentTimeMillis();
    int iterations = 0;
    end debug */

        mTimeAccumulator += adjustedDeltaTime;

        double tension = mSpringConfig.tension;
        double friction = mSpringConfig.friction;

        double position = mCurrentState.position;
        double velocity = mCurrentState.velocity;
        double tempPosition = mTempState.position;
        double tempVelocity = mTempState.velocity;

        double aVelocity, aAcceleration;
        double bVelocity, bAcceleration;
        double cVelocity, cAcceleration;
        double dVelocity, dAcceleration;

        double dxdt, dvdt;

        // iterate over the true time
        while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) {
      /* begin debug
      iterations++;
      end debug */
            mTimeAccumulator -= SOLVER_TIMESTEP_SEC;

            if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) {
                // This will be the last iteration. Remember the previous state in case we need to
                // interpolate
                mPreviousState.position = position;
                mPreviousState.velocity = velocity;
            }

            // Perform an RK4 integration to provide better detection of the acceleration curve via
            // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation
            // of the next and taking a weighted sum of the 4 derivatives as the final output.

            // This math was inlined since it made for big performance improvements when advancing several
            // springs in one pass of the BaseSpringSystem.

            // The initial derivative is based on the current velocity and the calculated acceleration
            aVelocity = velocity;
            aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity;

            // Calculate the next derivatives starting with the last derivative and integrating over the
            // timestep
            tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5;
            tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
            bVelocity = tempVelocity;
            bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

            tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5;
            tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5;
            cVelocity = tempVelocity;
            cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

            tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC;
            tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC;
            dVelocity = tempVelocity;
            dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity;

            // Take the weighted sum of the 4 derivatives as the final output.
            dxdt = 1.0 / 6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity);
            dvdt = 1.0 / 6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration);

            position += dxdt * SOLVER_TIMESTEP_SEC;
            velocity += dvdt * SOLVER_TIMESTEP_SEC;
        }

        mTempState.position = tempPosition;
        mTempState.velocity = tempVelocity;

        mCurrentState.position = position;
        mCurrentState.velocity = velocity;

        if (mTimeAccumulator > 0) {
            interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC);
        }

        // End the spring immediately if it is overshooting and overshoot clamping is enabled.
        // Also make sure that if the spring was considered within a resting threshold that it's now
        // snapped to its end value.
        if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) {
            // Don't call setCurrentValue because that forces a call to onSpringUpdate
            mStartValue = mEndValue;
            mCurrentState.position = mEndValue;
            setVelocity(0);
            isAtRest = true;
        }

    /* begin debug
    long endTime = System.currentTimeMillis();
    long elapsedMillis = endTime - startTime;
    Log.d(TAG,
        "iterations:" + iterations +
            " iterationTime:" + elapsedMillis +
            " position:" + mCurrentState.position +
            " velocity:" + mCurrentState.velocity +
            " realDeltaTime:" + realDeltaTime +
            " adjustedDeltaTime:" + adjustedDeltaTime +
            " isAtRest:" + isAtRest +
            " wasAtRest:" + mWasAtRest);
    end debug */

        // NB: do these checks outside the loop so all listeners are properly notified of the state
        //     transition
        boolean notifyActivate = false;
        if (mWasAtRest) {
            mWasAtRest = false;
            notifyActivate = true;
        }
        boolean notifyAtRest = false;
        if (isAtRest) {
            mWasAtRest = true;
            notifyAtRest = true;
        }
        for (SpringListener listener : mListeners) {
            // starting to move
            if (notifyActivate) {
                listener.onSpringActivate(this);
            }

            // updated
            listener.onSpringUpdate(this);

            // coming to rest
            if (notifyAtRest) {
                listener.onSpringAtRest(this);
            }
        }
    }

    /**
     * Check if this spring should be advanced by the system.  * The rule is if the spring is
     * currently at rest and it was at rest in the previous advance, the system can skip this spring
     *
     * @return should the system process this spring
     */
    public boolean systemShouldAdvance() {
        return !isAtRest() || !wasAtRest();
    }

    /**
     * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending
     * callbacks are fired as the spring comes to a rest.
     *
     * @return true if the spring was at rest in the prior iteration
     */
    public boolean wasAtRest() {
        return mWasAtRest;
    }

    /**
     * check if the current state is at rest
     *
     * @return is the spring at rest
     */
    public boolean isAtRest() {
        return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold &&
                getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold;
    }

    /**
     * Set the spring to be at rest by making its end value equal to its current value and setting
     * velocity to 0.
     */
    public Spring setAtRest() {
        mEndValue = mCurrentState.position;
        mTempState.position = mCurrentState.position;
        mCurrentState.velocity = 0;
        return this;
    }

    /**
     * linear interpolation between the previous and current physics state based on the amount of
     * timestep remaining after processing the rendering delta time in timestep sized chunks.
     *
     * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state
     */
    private void interpolate(double alpha) {
        mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position * (1 - alpha);
        mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity * (1 - alpha);
    }

    /**
     * add a listener
     *
     * @param newListener to add
     * @return the spring for chaining
     */
    public Spring addListener(SpringListener newListener) {
        if (newListener == null) {
            throw new IllegalArgumentException("newListener is required");
        }
        mListeners.add(newListener);
        return this;
    }

    /** listeners **/

    /**
     * remove a listener
     *
     * @param listenerToRemove to remove
     * @return the spring for chaining
     */
    public Spring removeListener(SpringListener listenerToRemove) {
        if (listenerToRemove == null) {
            throw new IllegalArgumentException("listenerToRemove is required");
        }
        mListeners.remove(listenerToRemove);
        return this;
    }

    /**
     * remove all of the listeners
     *
     * @return the spring for chaining
     */
    public Spring removeAllListeners() {
        mListeners.clear();
        return this;
    }

    /**
     * This method checks to see that the current spring displacement value is equal to the input,
     * accounting for the spring's rest displacement threshold.
     *
     * @param value The value to compare the spring value to
     * @return Whether the displacement value from the spring is within the bounds of the compare
     * value, accounting for threshold
     */
    public boolean currentValueIsApproximately(double value) {
        return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold();
    }

    // storage for the current and prior physics state while integration is occurring
    private static class PhysicsState {
        double position;
        double velocity;
    }

}

